﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.ArchitectureTools.CodeRepositoryQuery;
using UCL.Powersource.Analysis;

namespace UCL.Powersource.Statistics.CodeIndex
{
    /*
     * The classes contained in this file are designed following the following
     * patterns:
     * 
     *  - Strategy Pattern
     *  - Singleton
     *  
     */

    /// <remarks>
    /// Protection level: Namespace, I only want this enum to be accessed by
    /// classes of the UCL.Powersource.Statistics.CodeIndex namespace
    /// </remarks>
    public enum StatisticTypeValue
    {
        Maximum, Average, Median, Minimum
    }

    public enum StatisticDefinition
    {
        RFC
        }

    /// <remarks>
    /// Instead of using an Interface as specified by the Strategy design pattern
    /// we use an abstract class (which still can be seen as an interface) but
    /// initializing the code index on its constructor from the dummy classes 
    /// contained in Powersource; this is done like this for two reasons:
    /// 
    ///     1. All calculations use the same code index info, and;
    ///     2. When we are ready to retrieve the code index from another source
    ///        that is not the dummy classes, we only modify this place.
    /// </remarks>
    public abstract class CalculationBehavior
    {
        protected RepositoryEntities codeIndex;
        private   IAnalysisBridge analysisBridge;
        
        private void RetrieveCodeIndex(ITfsTeamProjectConfiguration teamProjConfig, ITimeFrame timeFrame)
        {
            //!!!!!!!!!!! codeIndex was never disposed - Eventually this would block connections to SQLSERVER 
            //- now getting centralized codeIndex from analysis bridge, DO NOT DISPOSE

            codeIndex = CodeIndexConnectionFactory.RetrieveCodeIndexFromAnalysisBridge(teamProjConfig, timeFrame);

            //if (codeIndex == null)
            //{
                analysisBridge = (IAnalysisBridge)teamProjConfig.ProjectAnalysisBridge;
                codeIndex = analysisBridge.RetrieveCodeIndex(timeFrame); 
                
                // We won't modify the code index.
                /*codeIndex.Assemblies.MergeOption = System.Data.Objects.MergeOption.NoTracking;
                codeIndex.AssemblyReferences.MergeOption = System.Data.Objects.MergeOption.NoTracking;
                codeIndex.Namespaces.MergeOption = System.Data.Objects.MergeOption.NoTracking;
                codeIndex.Types.MergeOption = System.Data.Objects.MergeOption.NoTracking;
                codeIndex.TypeDefinitions.MergeOption = System.Data.Objects.MergeOption.NoTracking;
                codeIndex.MethodDefinitions.MergeOption = System.Data.Objects.MergeOption.NoTracking;
                codeIndex.Fields.MergeOption = System.Data.Objects.MergeOption.NoTracking;
                codeIndex.Properties.MergeOption = System.Data.Objects.MergeOption.NoTracking;
                codeIndex.Events.MergeOption = System.Data.Objects.MergeOption.NoTracking;*/
            //}
        }

        public Dictionary<StatisticTypeValue, double?> calculate(
            ITfsTeamProjectConfiguration teamProjConfig, ITimeFrame timeFrame)
        {
            RetrieveCodeIndex(teamProjConfig, timeFrame);
            return getStatisticResultsDictionary();
        }

        protected abstract Dictionary<StatisticTypeValue, double?> getStatisticResultsDictionary();

        protected List<TypeDefinition> getClasses()
        {
            HashSet<long> compilerAttributes = new HashSet<long>();
            List<TypeDefinition> classes = new List<TypeDefinition>();
            Dictionary<long, TypeDefinition> typeDefinitions = new Dictionary<long, TypeDefinition>();

            // Number of types defined in the indexed assemblies. We exclude here the compiler generated types.
            IQueryable<TypeDefinition> definedTypes = 
                codeIndex.TypeDefinitions.Where(t => !t.IsRuntimeSpecialName && !t.IsToolSpecialName);
            
            // get the ids of the classes that were created by the compiler and 
            // place them in a Hash Set to avoid having duplicates
            try
            {
                foreach (CompilerGeneratedAttribute compilerAttribute in codeIndex.CompilerGeneratedAttributes)
                {
                    compilerAttributes.Add(compilerAttribute.TargetId);
                }
            }
            catch (Exception) { }

            // create a dictionary where the key is the class' id and the stored
            // value is a reference to that class
            foreach (TypeDefinition d in definedTypes)
            {
                typeDefinitions.Add(d.Id, d);
            }

            // exclude form the dictionary those classes whose Id can be found
            // in the set of compiler-generated attributes (classes)
            foreach (long id in typeDefinitions.Keys.Except(compilerAttributes))
            {
                classes.Add(typeDefinitions[id]);
            }

            return classes;
        }
    }


    public class RFCCalculationBehavior : CalculationBehavior
    {
        protected override Dictionary<StatisticTypeValue, double?> getStatisticResultsDictionary()
        {
            int elementNumber = 0;
            int publicMethodsCount = 0;            

            Dictionary<StatisticTypeValue, double?> statisticResults =
                new Dictionary<StatisticTypeValue, double?>();

            List<TypeDefinition> classes = getClasses();

            // we only care obout public classes for this statistic
            IEnumerable<TypeDefinition> filteredClasses = 
                classes.Where(filtered => filtered.Access.ToString() == "Public");

            int total = filteredClasses.Count();
            int[] muestra = new int[total];

            foreach (TypeDefinition typeDef in filteredClasses)
            {
                // we only count public methods that are not events or 
                // methods created by the compiler
                publicMethodsCount =
                    typeDef.Type.Methods.Where(m => m.MethodDefinition.ContainingEvent == null &&
                        m.MethodDefinition.Access.ToString() == "Public" &&
                        (m.MethodDefinition.ContainingProperty == null && !m.MethodDefinition.IsToolSpecialName) ||
                        (m.MethodDefinition.ContainingProperty != null && m.MethodDefinition.IsToolSpecialName)).Count();

                muestra[elementNumber++] = publicMethodsCount;
            }

            if (total > 0)
            {
                IOrderedEnumerable<int> orderedMuestra = muestra.OrderBy(m => m);
                muestra = orderedMuestra.ToArray();

                statisticResults.Add(StatisticTypeValue.Maximum, muestra[total - 1]);
                statisticResults.Add(StatisticTypeValue.Average, (double)muestra.Sum() / total);
                statisticResults.Add(StatisticTypeValue.Median, muestra[(int)total / 2]);
                statisticResults.Add(StatisticTypeValue.Minimum, muestra[0]);
            }
            else
            {
                statisticResults.Add(StatisticTypeValue.Maximum, null);
                statisticResults.Add(StatisticTypeValue.Average, null);
                statisticResults.Add(StatisticTypeValue.Median,  null);
                statisticResults.Add(StatisticTypeValue.Minimum, null);
            }

            return statisticResults;
        }
    }

    /// <remarks>
    /// What should be the protection level for this class? 
    /// 
    /// It makes no sense to have a private class since it would not be accessed
    /// from anywhere. It makes more sense to have an inner class declared as 
    /// private since it could be accesed only by the class who contains it.
    /// </remarks>
    public abstract class Calculator
    {
        protected CalculationBehavior _calculationBehavior;
        //protected Dictionary<StatisticTypeValue, StatisticTeamBase> _statistics;

        protected Dictionary<StatisticTypeValue, 
                    Dictionary<ITfsTeamProjectConfiguration, 
                        Dictionary<ITimeFrame, StatisticTeamResult>>> _statisticsCache;

        protected static Dictionary<StatisticTypeValue, double?> staticResults = new Dictionary<StatisticTypeValue, double?>();

        public Calculator()
        {
            //_statisticsCache = new Dictionary<StatisticTypeValue, StatisticTeamBase>();
            _statisticsCache = new Dictionary<StatisticTypeValue, 
                                    Dictionary<ITfsTeamProjectConfiguration, 
                                        Dictionary<ITimeFrame, StatisticTeamResult>>>();
        }

        public double? calculateStatistic(
            ITfsTeamProjectConfiguration teamProjConfig, ITimeFrame timeFrame, StatisticTypeValue typeValue)
        {
            List<StatisticTypeValue> typeValueList =
                Enum.GetValues(typeof(StatisticTypeValue))
                    .Cast<StatisticTypeValue>()
                    .ToList<StatisticTypeValue>();

            // If the dictionary cache is empty or both the value does not exist in
            // the dictionary and the time frame is not empty, then calculate: 
            if(_statisticsCache.Count() == 0 || 
                (!valueExistsInDictionary(teamProjConfig, timeFrame, typeValue) /*&&
                 !timeFrameIsEmpty(teamProjConfig, timeFrame, typeValue)*/))
            {
                // call the algorithm encapsulated by an implementation of the
                // ICalculationBehavior interface following the     
                staticResults = _calculationBehavior.calculate(teamProjConfig, timeFrame);

                foreach (StatisticTypeValue statisticType in typeValueList)
                {
                    if (_statisticsCache.Count() == 0 || !_statisticsCache.ContainsKey(statisticType))
                        //_statisticsCache.Add(statisticType, returnNewStatisticInstance(statisticType));
                        _statisticsCache.Add(statisticType, 
                            new Dictionary<ITfsTeamProjectConfiguration, Dictionary<ITimeFrame, StatisticTeamResult>>());

                    //_statisticsCache[statisticType].Resolve(teamProjConfig, timeFrame);
                    createStructureForValue(statisticType, teamProjConfig, timeFrame, staticResults[statisticType]);
                }
            }

            /*if (timeFrameIsEmpty(teamProjConfig, timeFrame, typeValue))
            {
                if (!staticResults.ContainsKey(typeValue))
                {
                    return null;
                }
                else
                return staticResults[typeValue];
            }*/

            // by the time we hit this line, we guarantee that the statistic
            // value has already been stored in the statisticsCache dictionary
            return _statisticsCache[typeValue][teamProjConfig][timeFrame].Value;
        }

        private void createStructureForValue(StatisticTypeValue statisticType,
            //Dictionary<ITfsTeamProjectConfiguration, Dictionary<ITimeFrame, StatisticTeamResult>> dictionary, 
            ITfsTeamProjectConfiguration teamProjConfig, ITimeFrame timeFrame, double? value)
        {
            var dictionary = _statisticsCache[statisticType];
            
            if(!dictionary.ContainsKey(teamProjConfig))
            {
                dictionary = new Dictionary<ITfsTeamProjectConfiguration,Dictionary<ITimeFrame,StatisticTeamResult>>();
                dictionary.Add(teamProjConfig, new Dictionary<ITimeFrame,StatisticTeamResult>());
            }

            if(!dictionary[teamProjConfig].ContainsKey(timeFrame))
            {
                var result = new StatisticTeamResult(teamProjConfig, timeFrame, value);

                // It could be the case that the time dictionary for the time
                // frame exists, so there's no need to create another one
                if(dictionary[teamProjConfig] == null)
                    dictionary[teamProjConfig] = new Dictionary<ITimeFrame,StatisticTeamResult>();

                dictionary[teamProjConfig].Add(timeFrame, result);
            }

            _statisticsCache[statisticType] = dictionary;
        }

        private bool valueExistsInDictionary(
            ITfsTeamProjectConfiguration teamProjConfig, ITimeFrame timeFrame, StatisticTypeValue statisticType)
        {
            var dictionary = _statisticsCache[statisticType];

            //if (teamResults.ContainsKey(teamProjConfig))
            if(dictionary.ContainsKey(teamProjConfig))
                //if (teamResults[teamProjConfig].Count() > 0 && teamResults[teamProjConfig].ContainsKey(timeFrame))
                if (dictionary[teamProjConfig].Count() > 0 && dictionary[teamProjConfig].ContainsKey(timeFrame))
                    return true;

            return false;
        }

        //protected abstract StatisticTeamBase returnNewStatisticInstance(StatisticTypeValue typeValue);
    }

    [DataContract(IsReference = true)]
    public class RFCCalculator : Calculator
    {
        public RFCCalculator()
        {
            _calculationBehavior = new RFCCalculationBehavior();
        }

        /*protected override StatisticTeamBase returnNewStatisticInstance(StatisticTypeValue typeValue)
        {
            switch (typeValue)
            {
                case StatisticTypeValue.Maximum:
                    return new RFCStatisticMaximum();

                case StatisticTypeValue.Minimum:
                    return new RFCStatisticMinimum();

                case StatisticTypeValue.Average:
                    return new RFCStatisticAverage();

                case StatisticTypeValue.Median:
                    return new RFCStatisticMedian();
            }

            return null;
        }*/
    }

    
    public class CalculatorFactory
    {
        // Calculator used for the Response For Class statistic
        private static RFCCalculator _rfcCalculator;

        private CalculatorFactory() { }

        public static Calculator getCalculator(StatisticDefinition statDefinition)
        {
            switch (statDefinition)
            {
                case StatisticDefinition.RFC:
                    return new RFCCalculator();
            }

            return null;
        }

        public static RFCCalculator getRFCCalculator()
        {
            if (_rfcCalculator == null)
                _rfcCalculator = new RFCCalculator();

            return _rfcCalculator;
        }
    }

    public class CodeIndexConnectionFactory
    {
        private static Dictionary<ITfsTeamProjectConfiguration,
            Dictionary<ITimeFrame, RepositoryEntities>> _ConnectionDictionary =
                new Dictionary<ITfsTeamProjectConfiguration, Dictionary<ITimeFrame, RepositoryEntities>>();

        private CodeIndexConnectionFactory() { }

        public static RepositoryEntities RetrieveCodeIndexFromAnalysisBridge(ITfsTeamProjectConfiguration teamProjConfig, ITimeFrame timeFrame)
        {

            if (!_ConnectionDictionary.ContainsKey(teamProjConfig))
            {
                _ConnectionDictionary.Add(teamProjConfig, 
                    new Dictionary<ITimeFrame,RepositoryEntities>());
            }

            if (!_ConnectionDictionary[teamProjConfig].ContainsKey(timeFrame))
            {
                if(_ConnectionDictionary[teamProjConfig] == null)
                    _ConnectionDictionary[teamProjConfig] = new Dictionary<ITimeFrame, RepositoryEntities>();

                var analysisBridge = (IAnalysisBridge)teamProjConfig.ProjectAnalysisBridge;
                var codeIndex = analysisBridge.RetrieveCodeIndex(timeFrame);

                _ConnectionDictionary[teamProjConfig].Add(timeFrame, codeIndex);
            }

            return _ConnectionDictionary[teamProjConfig][timeFrame];
        }
    }
}
